<template>
  <canvas ref="container" class="w-full h-full"></canvas>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { WebGPU } from "../../lib/webgpu/WebGPU";
import { getMvpMatrix } from "../../lib/math/GetMatrix";

const container = ref(null);
const webgpu = new WebGPU();
let oldTime = Date.now();

const cube = {
  vertex: new Float32Array([
    // float3 position, float2 uv
    // face1
    +1, -1, +1, 1, 1, -1, -1, +1, 0, 1, -1, -1, -1, 0, 0, +1, -1, -1, 1, 0, +1,
    -1, +1, 1, 1, -1, -1, -1, 0, 0,
    // face2
    +1, +1, +1, 1, 1, +1, -1, +1, 0, 1, +1, -1, -1, 0, 0, +1, +1, -1, 1, 0, +1,
    +1, +1, 1, 1, +1, -1, -1, 0, 0,
    // face3
    -1, +1, +1, 1, 1, +1, +1, +1, 0, 1, +1, +1, -1, 0, 0, -1, +1, -1, 1, 0, -1,
    +1, +1, 1, 1, +1, +1, -1, 0, 0,
    // face4
    -1, -1, +1, 1, 1, -1, +1, +1, 0, 1, -1, +1, -1, 0, 0, -1, -1, -1, 1, 0, -1,
    -1, +1, 1, 1, -1, +1, -1, 0, 0,
    // face5
    +1, +1, +1, 1, 1, -1, +1, +1, 0, 1, -1, -1, +1, 0, 0, -1, -1, +1, 0, 0, +1,
    -1, +1, 1, 0, +1, +1, +1, 1, 1,
    // face6
    +1, -1, -1, 1, 1, -1, -1, -1, 0, 1, -1, +1, -1, 0, 0, +1, +1, -1, 1, 0, +1,
    -1, -1, 1, 1, -1, +1, -1, 0, 0,
  ]),
  vertexCount: 36,
};

async function initPipeline(device, format, size) {
  let pipeline = await device.createRenderPipelineAsync({
    layout: "auto",
    vertex: {
      module: device.createShaderModule({
        code: `
            @binding(0) @group(0) var<uniform> mvpMatrix : mat4x4<f32>;
  
            struct VertexOutput {
                @builtin(position) Position : vec4<f32>,
                @location(0) fragUV : vec2<f32>,
                @location(1) fragPosition: vec4<f32>
            };
  
            @vertex
            fn main(
                @location(0) position : vec4<f32>,
                @location(1) uv : vec2<f32>
            ) -> VertexOutput {
                var output : VertexOutput;
                output.Position = mvpMatrix * position;
                output.fragUV = uv;
                output.fragPosition = (position + vec4<f32>(1.0, 1.0, 1.0, 1.0)) * 0.5;
                return output;
            }
          `,
      }),
      entryPoint: "main",
      buffers: [
        {
          arrayStride: 5 * 4,
          attributes: [
            {
              shaderLocation: 0,
              offset: 0,
              format: "float32x3",
            },
            {
              shaderLocation: 1,
              offset: 3 * 4,
              format: "float32x2",
            },
          ],
        },
      ],
    },
    fragment: {
      module: device.createShaderModule({
        code: `
            @group(1) @binding(0) var Sampler: sampler;
            @group(1) @binding(1) var Texture: texture_2d<f32>;
            
            @fragment
            fn main(
                @location(0) fragUV: vec2<f32>,
                @location(1) fragPosition: vec4<f32>
            ) -> @location(0) vec4<f32> {
                return textureSample(Texture, Sampler, fragUV) * fragPosition;
            }
          `,
      }),
      entryPoint: "main",
      targets: [
        {
          format: format,
        },
      ],
    },
    primitive: {
      topology: "triangle-list",
      cullMode: "back",
      frontFace: "ccw",
    },
    depthStencil: {
      depthWriteEnabled: true,
      depthCompare: "less",
      format: "depth24plus",
    },
  });

  let depthTexture = device.createTexture({
    size,
    format: "depth24plus",
    // eslint-disable-next-line no-undef
    usage: GPUTextureUsage.RENDER_ATTACHMENT,
  });
  let depthView = depthTexture.createView();

  let vertexBuffer = device.createBuffer({
    label: "GPUBuffer store vertex",
    size: cube.vertex.byteLength,
    // eslint-disable-next-line no-undef
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(vertexBuffer, 0, cube.vertex);

  let mvpBuffer = device.createBuffer({
    label: "GPUBuffer store 4x4 matrix",
    size: 4 * 4 * 4,
    // eslint-disable-next-line no-undef
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
  let uniformGroup = device.createBindGroup({
    label: "Uniform Group with Matrix",
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      {
        binding: 0,
        resource: {
          buffer: mvpBuffer,
        },
      },
    ],
  });

  return {
    pipeline,
    vertexBuffer,
    depthTexture,
    depthView,
    uniformGroup,
    mvpBuffer,
  };
}

async function initTexture(device, pipelineObj) {
  let res = await fetch("./images/ams.webp");
  let img = await res.blob();
  let bitmap = await createImageBitmap(img);
  let textureSize = [bitmap.width, bitmap.height];

  let texture = device.createTexture({
    size: textureSize,
    format: "rgba8unorm",
    usage:
      // eslint-disable-next-line no-undef
      GPUTextureUsage.TEXTURE_BINDING |
      // eslint-disable-next-line no-undef
      GPUTextureUsage.COPY_DST |
      // eslint-disable-next-line no-undef
      GPUTextureUsage.RENDER_ATTACHMENT,
  });

  device.queue.copyExternalImageToTexture(
    { source: bitmap },
    { texture: texture },
    textureSize
  );

  let sampler = device.createSampler({
    magFilter: "linear",
    minFilter: "linear",
  });

  let textureGroup = device.createBindGroup({
    label: "Texture Group with Texture/Sampler",
    layout: pipelineObj.pipeline.getBindGroupLayout(1),
    entries: [
      {
        binding: 0,
        resource: sampler,
      },
      {
        binding: 1,
        resource: texture.createView(),
      },
    ],
  });

  return textureGroup;
}

function draw(device, context, pipelineObj, textureGroup) {
  const commandEncoder = device.createCommandEncoder();
  const renderPassDescriptor = {
    colorAttachments: [
      {
        view: context.getCurrentTexture().createView(),
        clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
        loadOp: "clear",
        storeOp: "store",
      },
    ],
    depthStencilAttachment: {
      view: pipelineObj.depthView,
      depthClearValue: 1.0,
      depthLoadOp: "clear",
      depthStoreOp: "store",
    },
  };
  const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
  passEncoder.setPipeline(pipelineObj.pipeline);
  passEncoder.setVertexBuffer(0, pipelineObj.vertexBuffer);
  passEncoder.setBindGroup(0, pipelineObj.uniformGroup);
  passEncoder.setBindGroup(1, textureGroup);
  passEncoder.draw(cube.vertexCount);
  passEncoder.end();
  device.queue.submit([commandEncoder.finish()]);
}

async function init() {
  let canvas = container.value;
  let { device, context, format, size } = await webgpu.initWebGPU(canvas);
  let pipelineObj = await initPipeline(device, format, size);
  let textureGroup = await initTexture(device, pipelineObj);

  let aspect = size.width / size.height;
  let position = { x: 0, y: 0, z: -5 };
  let rotation = { x: 0.5, y: 0.5, z: 0 };
  let scale = { x: 1, y: 1, z: 1 };

  function frame() {
    let newTime = Date.now();
    let delta = (newTime - oldTime) / 1000;
    oldTime = newTime;

    rotation.x += delta * 1;
    rotation.y += delta * 1;
    rotation.z += delta * 1;
    let mvpMatrix = getMvpMatrix(aspect, position, rotation, scale);
    device.queue.writeBuffer(pipelineObj.mvpBuffer, 0, mvpMatrix.buffer);
    draw(device, context, pipelineObj, textureGroup);

    requestAnimationFrame(frame);
  }
  frame();

  window.addEventListener("resize", () => {
    size.width = canvas.width = canvas.clientWidth;
    size.height = canvas.height = canvas.clientHeight;
    pipelineObj.depthTexture.destroy();
    pipelineObj.depthTexture = device.createTexture({
      size,
      format: "depth24plus",
      // eslint-disable-next-line no-undef
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });
    pipelineObj.depthView = pipelineObj.depthTexture.createView();
    aspect = size.width / size.height;
  });
}

onMounted(() => {
  init();
});
</script>
